/**
 * 辅助函数
 */
function $(selector) {
    return document.querySelector(selector);
}
function appendToHead(tagName, type, html, src) {
    const dom = document.createElement(tagName);
    dom.innerHTML = html || '';
    dom.type = type;
    dom.href = dom.src = src;
    $('head').appendChild(dom);
}
function addTestNode(title, id){
    var testNode = document.querySelector(".test-model-html").cloneNode(true);
    testNode.classList.remove('test-model-html');
    testNode.classList.remove('model-html');
    testNode.querySelector('th').textContent = title;
    testNode.id = `tr-${id}`;
    document.querySelector('#performance-tbody').append(testNode);
}
const appendScriptToHead = (src) => appendToHead('script', 'text/javascript' , '', src);

/**
 * 将加解密的公共函数抽取出来
 */
// 将key导入指定的算法中
async function getImportKey(dom, algorithm, showErr, length) {
    let rawKey = getLengthEncodeStr(dom, length);
    try {
        return await window.crypto.subtle.importKey(
            "raw",
            rawKey.buffer,
            algorithm,
            false,
            ["encrypt", "decrypt"]
        );
    } catch (e) {
        console.error(e)
        if(e.toString().includes('Algorithm: Unrecognized name')){
            showErr('暂不支持' + algorithm + '算法');
        }
    }
};

function getEncodeStr(dom, length){
    dom.value = dom.value.padEnd(length, '0');;
    return encode(dom.value);
}
async function generateKey(dom, algorithm, length) {
    const key = await window.crypto.subtle.generateKey(
        {
            name: algorithm,
            length
        },
        true,
        ["encrypt", "decrypt"]
    );
    dom.value = arrayBufferToHex(await window.crypto.subtle.exportKey(
        "raw",
        key
    ));
    return key;
}
// 获取指定长度的 Uint8Array，不足时自动补全，超过从前面开始取（默认为16位）
// 字符串若只包含数字和逗号，当作Uint8Array使用
// 将最终加密参数回填到输入框
function getLengthEncodeStr(dom, len = 16){
    let rst = tmpStr = dom.value;
    if(rst) {
        if(rst.length==(len*2) && /^[A-Fa-f0-9]+$/.test(rst)) { // 若为hex，将其转为ArrayBuffer
            rst = hexToArrayBuffer(rst);
        }else if(rst.length==Math.ceil(len/3)*4 && /^[A-Za-z0-9+/=]+$/.test(rst)) { // 若为base64，将其转为ArrayBuffer
            rst = base64ToArrayBuffer(rst);
        }else{ //当作纯字符串使用
            rst = rst.length < len ? rst.padEnd(len, '0') : rst.substring(0,len);
            tmpStr = rst;
            rst = encode(rst);
        }
    }else{ // 空则使用默认字符串
        tmpStr = dom.className.includes('key') ? ''.padEnd(len, '0') : '8a6c4ddd8a6c4ddd';
        rst = base64ToArrayBuffer(btoa(tmpStr));
    }
    dom.value = tmpStr || arrayBufferToHex(rst); // 将 KEY 或 IV 的值重新赋回到页面中
    return rst.slice(0,len);
}

// 获取测试的方法
function getTestFn(fn){
    async function testFn(ciphertext, times=1, cb) { // 用于按次数进行测试
        var beginTime = window.performance.now();
        for(var i=0; i<times; i++) {
            await fn(ciphertext);
        }
        const duration = window.performance.now() - beginTime;
        cb && cb(duration);
        return duration;
    }
    testFn.test = fn; // 用于按时间进行测试
    return testFn;
}

const [encode, decode] = (()=>{
    // 将字符串编码为 Uint8Array
    let enc = new TextEncoder();
    function encode(message) {
        return enc.encode(message);
    }
    let dec = new TextDecoder();
    // 将字符串编码为 Uint8Array
    function decode(message) {
        return dec.decode(message);
    }
    return [encode, decode];
})();


// 一键性能测试需要运行的测试列表
window.allPerformance = []; // {name, fn}

// 生成测试数据
function initPerformanceData(cb){
    const performanceData = [];
    setTimeout(()=>{
        const getSizeData = (times64K)=>{
            var d = [];
            for(var i=0;i<times64K;i++){ 
                d.push(...[...window.crypto.getRandomValues(new Uint8Array(65536))]); //65536，64KB
            }
            return new Uint8Array(d);
        }
        performanceData.push({name:'data4K',data: window.crypto.getRandomValues(new Uint8Array(4096))});
        performanceData.push({name:'data64K',data: getSizeData(1)});
        performanceData.push({name:'data128K',data: getSizeData(2)});
        performanceData.push({name:'data512K',data: getSizeData(8)});
        performanceData.push({name:'data1024K',data: getSizeData(16)}); //生成 1M 数据 16*64 = 1024 KB
        window.performanceData = performanceData;
        cb && cb();
    }, 20);
}


async function startAllPerformace(){
    if(!window.durationPerformance) {
        document.querySelectorAll('.encrypt-button').forEach(n=>n.click());

        window.durationPerformance = true;
        $('.start-all-performance-button').style.background = '#ccc';
        $('.start-all-performance-button').textContent = '重新测试';
        [].forEach.call($('.all-performance table').querySelectorAll('td'), function(td) {
            td.textContent = '';
        });
        // 测试结束，重置
        const resetFn = () => {
            window.durationPerformance = false;
            stopPerformanceBtn.textContent = '停止';
            $('.start-all-performance-button').style.background = '#2525ea';
            $("#performancing").textContent = `测试完成！`;
        }
        try {
            // 测试后将结果显示到页面上
            function showTestRst(d, testTimes, time, resultId, j){
                const dataSize = Number(d.name.replace('data','').replace('K',''));
                let rst = String(dataSize * testTimes / 1024 / time * 1000 * 8);

                if(rst.indexOf('.') > -1) {
                    rst = rst.substring(0, rst.indexOf('.') + 3);
                }else{
                    rst += '.00';
                }
                $(resultId).querySelectorAll('td')[j].textContent = rst;
                if(window.interruptPerformance) {
                    resetFn();
                    throw '用户手动停止了一键性能测试';
                }
            }
            switch($('#testType').value){
                case '0': //按次数
                    let testTimes = $('#testTimes').value || 100; // 默认检测100次
                    testTimes = testTimes>3000 ? 3000 : testTimes<10 ? 10 : testTimes; // 测试最少1次，最多3000次
                    $('#testTimes').value = testTimes;

                    for(let i=0, len=window.allPerformance.length; i<len; i++){
                        const algorithm = window.allPerformance[i]
                        $("#performancing").textContent = `测试${algorithm.name}...`;
                        for(let j = 0, l = window.performanceData.length; j<l; j++){
                            const [d, {fn, encryptFn, resultId}] = [window.performanceData[j], algorithm];
                            
                            const time = String(await fn(encryptFn ? await encryptFn(d.data) : d.data, testTimes));
                            showTestRst(d, testTimes, time, resultId, j);
                        }
                    }
                    resetFn();
                    break;
                case '1': // 按时间 使用递归回调，确保同一时间只有一个算法在执行。
                    let testTime = $('#testTime').value || 200; // 默认100ms
                    testTime = testTime>10000 ? 10000 : testTime<100 ? 100 : testTime;
                    $('#testTime').value = testTime;

                    const testFn = async (algorithms, datas, i, j)=>{
                        if(j < datas.length){ // 继续执行本轮
                            $("#performancing").textContent = `测试${algorithms[i].name}...`;
                            let flag = true, runTimes = 0, trueTime = window.performance.now();
                            const [d, {fn, encryptFn, resultId}] = [window.performanceData[j], window.allPerformance[i]];
                            setTimeout(async ()=>{ // setTimeout 必须在while之前执行，否则 while 无法停止。
                                flag = false;
                                trueTime = window.performance.now() - trueTime;

                                // 控制算法执行出错时（如不支持的算法）不显示到页面，不继续往下走（按时间执行的时候，try-catch无法捕获递归回调中的错误，需要单独处理）
                                if(runTimes > 0){ 
                                    showTestRst(d, runTimes, trueTime, resultId, j)
                                    await testFn(algorithms, datas, i, ++j);
                                }else{
                                    resetFn();
                                }
                            }, testTime);
                            const data = encryptFn ? await encryptFn(d.data) : d.data; // 若为解密，需要先获取加密数据
                            while(flag){
                                await fn.test(data);
                                runTimes++;
                            }
                        }else if(j == datas.length && i+1 < algorithms.length){ // 本轮执行完毕，进入下一轮
                            await testFn(algorithms, datas, ++i, 0)
                        }else if(i+1 == algorithms.length){
                            resetFn();
                        }
                    }
                    await testFn(window.allPerformance, window.performanceData, 0, 0);
                    break;
                case '2':
                    let testDataSize = $('#testDataSize').value || 100; // 默认100MB
                    testDataSize = testDataSize>2000 ? 2000 : testDataSize<50 ? 50 : testDataSize;
                    $('#testDataSize').value = testDataSize;

                    for(let i=0, len=window.allPerformance.length; i<len; i++){
                        const algorithm = window.allPerformance[i]
                        $("#performancing").textContent = `测试${algorithm.name}...`;
                        for(let j = 0, l = window.performanceData.length; j<l; j++){
                            const [d, {fn, encryptFn, resultId}] = [window.performanceData[j], algorithm];
                            
                            const size = Number(d.name.replace('data','').replace('K','')), 
                                  times = testDataSize * 1024 / size;
                            
                            const time = String(await fn(encryptFn ? await encryptFn(d.data) : d.data, times));
                            showTestRst(d, times, time, resultId, j);
                        }
                    }
                    resetFn();
                    break;
                default:
                    break;

            }
        } catch (error) {
            console.error(error);
            resetFn();
        }
        
    }
}
